feat(server/s3): support multipart upload - #2813
Conversation
Replace itsHenry35/gofakes3 with OpenListTeam/gofakes3 Signed-off-by: MadDogOwner <xiaoran@xrgzs.top>
- Implement gofakes3 MultipartBackend (Create/UploadPart/Complete/Abort) on s3Backend so multipart parts stream to local temp files instead of being buffered in memory - Track in-progress uploads via a new uploads sync.Map on s3Backend - Refactor the PutObject body into a reusable putStream helper shared with the multipart Complete path - Validate part ordering, etags and existence, reject short reads, and return S3-style multipart etags - Add end-to-end multipart tests against a real Local driver Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
- Track lastActivity on each multipart upload, updated on create and every part upload, so idle uploads can be detected - Add a background reaper per backend instance that removes uploads inactive for longer than the TTL (default 24h) and cleans their temp directories - Add a startup sweep that removes leftover s3-multipart-* directories older than the TTL, recovering part files from a previous crash - Add a configurable s3.multipart_ttl (env S3_MULTIPART_TTL) duration, parsed via time.ParseDuration with a 24h default - Add tests for TTL-based reaping and stale-directory cleanup Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
9a51b73 to
48f5af8
Compare
PIKACHUIM
left a comment
There was a problem hiding this comment.
🙏 感谢贡献
感谢 @xrgzs 提交此PR!我已完成代码评审,以下是评审结果。
📖 PR背景与需求
PR标题:feat(server/s3): support multipart upload
关联Issue:Related to #460
需求说明:为 OpenList 的 S3 服务器实现完整的分片上传(multipart upload)支持。此前 S3 分片上传会回退到 gofakes3 默认的内存缓冲器,导致大文件上传时可能耗尽内存;且客户端未完成的孤儿上传会一直占用磁盘空间。
预期目标:
- 分片按流式方式落盘到临时文件,内存占用恒定
- 自动回收被遗弃的分片上传(超过TTL未活动)
- 兼容现有的单分片上传行为
- 支持 awscli / s3cmd / rclone 等标准 S3 客户端
📋 问题摘要
- ✅ 功能性:完整实现了 S3 分片上传协议
- ✅ 代码质量:测试覆盖充分,实现规范
⚠️ 改进建议:有3处可优化点
📂 逐文件分析
go.mod & go.sum
改动意图:将依赖从 itsHenry35/gofakes3 v0.0.8 切换到 OpenListTeam/gofakes3 v0.8.0。
代码修改逻辑:
- 旧版本只支持内存缓冲的默认上传器
- OpenListTeam fork 新增了
MultipartBackend接口,允许后端自行流式处理分片 - 这是实现分片上传的前提
合理性评估:
- ✅ 优点:使用自己维护的 fork 可以快速修复问题和添加功能
⚠️ 疑问:是否考虑过向上游itsHenry35/gofakes3贡献这个功能?长期维护 fork 会增加维护负担
internal/conf/config.go
改动意图:新增配置项 MultipartTTL,用于设置分片上传的过期时间。
代码修改逻辑:
- 在
S3结构体中新增MultipartTTL string字段 - 支持通过环境变量
MULTIPART_TTL配置 - 默认值为 24 小时(在代码中处理)
合理性评估:
- ✅ 优点:可配置化设计,灵活性好
- ✅ 优点:默认值合理(24小时)
详细建议:
在注释中补充配置格式说明:
MultipartTTL string `json:"multipart_ttl" env:"MULTIPART_TTL"` // Duration string like "24h", "30m"; defaults to 24h if empty or invalidserver/s3/backend.go
改动意图:重构 s3Backend,添加分片上传支持。
代码修改逻辑:
- 新增字段:
uploads *sync.Map用于跟踪进行中的分片上传 - 提取共享逻辑:将
PutObject的主体逻辑抽取为putStream函数,使其可以被单分片上传和分片完成路径共享 - 启动清理协程:在
newBackend()中调用startReaper()启动后台清理协程
合理性评估:
- ✅ 优点:
putStream的抽取避免了代码重复,确保单分片和多分片上传遵循相同的业务规则(目录创建、元数据、忽略规则) - ✅ 优点:
PutObject的行为不变,向后兼容 ⚠️ 疑问:startReaper()启动的协程没有明确的停止机制,如果服务重启频繁可能会留下孤儿协程(虽然在实践中不太可能)
server/s3/multipart.go (新增,379行)
改动意图:实现 gofakes3.MultipartBackend 接口的四个核心方法。
代码修改逻辑:
-
CreateMultipartUpload:
- 在
conf.Conf.TempDir下创建s3-multipart-*临时目录 - 生成 UUID 作为
uploadID - 将
multipartState存入b.uploadsmap
- 在
-
UploadPart:
- 验证分片号在有效范围内(1 到 MaxUploadPartNumber)
- 将分片流式写入临时文件(
part-XXXXX) - 计算 MD5 并返回 quoted etag
- 验证
Content-Length与实际读取字节数匹配(防止截断) - 更新
lastActivity时间戳
-
CompleteMultipartUpload:
- 验证分片列表按升序排列
- 验证每个分片存在且 etag 匹配
- 按顺序打开所有分片文件,使用
io.MultiReader拼接 - 调用
putStream将拼接结果写入底层存储 - 计算并返回 S3 规范的 multipart etag:
"<md5ofmd5s>-N" - 成功后清理临时文件和bookkeeping
-
AbortMultipartUpload:
- 删除临时目录
- 从
b.uploads中移除记录 - 对未知 uploadID 保持幂等
-
Reaper机制:
- 每个 backend 实例启动一个后台协程
- 按
TTL/4(钳制在 [10s, 1h])的间隔执行清理 - 移除
lastActivity超过 TTL 的上传 - 启动时清理上次进程崩溃残留的
s3-multipart-*目录
合理性评估:
- ✅ 优点:完全符合 S3 分片上传协议规范
- ✅ 优点:短读检测(
ErrIncompleteBody)防止截断数据被静默接受 - ✅ 优点:Complete 失败时保留上传状态,允许客户端重试
- ✅ 优点:Abort 的幂等性设计符合最佳实践
- ✅ 优点:Reaper 机制自动清理孤儿上传,防止磁盘泄漏
⚠️ 疑问:io.MultiReader打开所有分片文件后,如果putStream失败,这些文件会被正确关闭吗?
详细建议:
- 资源清理保障:
当前代码在 Complete 中打开所有分片文件后,通过utils.NewReadCloser包装了一个统一的 Close 函数。但如果putStream内部发生 panic,这些文件可能不会被关闭。建议使用defer确保清理:
combined := utils.NewReadCloser(io.MultiReader(readers...), func() error {
var firstErr error
for _, c := range closers {
if err := c.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
})
defer combined.Close() // 确保无论如何都会关闭
err := b.putStream(ctx, bucket, object, state.meta, combined, total)
if err != nil {
return "", "", err
}- 并发安全说明:
代码注释提到"gofakes3 does not serialize multipart operations",建议在multipartState结构体定义处补充并发安全文档:
// multipartState tracks one in-progress multipart upload.
//
// Concurrency: gofakes3 does not serialize operations for the same uploadID.
// The parts map is protected by mu. Each part file is independent, so concurrent
// UploadPart calls for different part numbers are safe without additional locking.
type multipartState struct {
// ...
}server/s3/multipart_test.go (新增,321行)
改动意图:提供完整的分片上传功能测试覆盖。
代码修改逻辑:
TestMultipartUploadEndToEnd:端到端测试,覆盖创建、上传3个分片、完成、验证结果文件内容和etag- 还覆盖了错误场景:短读、未知uploadID、越界分片号、乱序完成、错误etag、缺失分片、失败的complete保留上传
TestMultipartAbort:测试abort删除临时目录和幂等性TestMultipartReapExpired:测试TTL超时的上传被回收,活跃上传保留TestMultipartCleanupStaleDirs:测试启动时清理残留目录
合理性评估:
- ✅ 优点:测试覆盖非常全面,包含正常流程和多种边界情况
- ✅ 优点:使用真实的 Local 驱动进行端到端测试,而非 mock
- ✅ 优点:测试用例有清晰的 setup/cleanup 逻辑
其他文件修改
改动意图:将所有文件中的 github.com/itsHenry35/gofakes3 import 路径替换为 github.com/OpenListTeam/gofakes3。
涉及文件:
server/s3/list.goserver/s3/logger.goserver/s3/pager.goserver/s3/redirect.goserver/s3/server.goserver/s3/utils.go
合理性评估:
- ✅ 优点:更新彻底,没有遗漏
🎯 总体评价
功能性:⭐⭐⭐⭐⭐ - 完整实现了 S3 分片上传协议,解决了内存和磁盘泄漏问题
安全性:⭐⭐⭐⭐⭐ - 输入验证充分,短读检测到位,无明显安全隐患
代码质量:⭐⭐⭐⭐⭐ - 代码结构清晰,测试覆盖完善,注释详尽
实现方案:⭐⭐⭐⭐⭐ - 流式处理分片避免内存峰值,Reaper机制优雅,资源管理到位
建议操作:
- ✅ Approve(强烈建议合并)
- 🔄 Request Changes(需要修改)
- ❌ Close(建议关闭)
理由:此 PR 是一个非常高质量的功能实现,完全解决了 S3 服务的关键痛点:
- 将大文件上传的内存占用从"与文件大小成正比"降至"恒定"
- 通过 Reaper 机制自动清理孤儿上传,防止磁盘泄漏
- 向后兼容现有单分片上传行为
- 测试覆盖全面(包括端到端、边界情况、错误处理)
- 代码质量极高(结构清晰、注释详尽、符合最佳实践)
建议的改进点(资源清理的 defer、注释补充)都是锦上添花,不阻碍合并。
💡 后续建议
- 文档更新:在 OpenList-Docs 中补充分片上传功能说明和
MULTIPART_TTL配置项 - 监控指标:考虑添加 Prometheus 指标,监控活跃上传数、清理的孤儿上传数等
- 上游贡献:如果时间允许,考虑向
itsHenry35/gofakes3贡献MultipartBackend接口,减少长期维护 fork 的负担 - 性能测试:在生产环境监控分片上传的性能表现(延迟、吞吐量、磁盘IO)
再次感谢你的精彩贡献!这是一个教科书级别的功能实现。👏
Summary / 摘要
为 fake S3 服务器实现分片上传(multipart upload)支持,并自动回收被遗弃的分片上传。本 PR 含三个提交:
refactor(server/s3): use OpenListTeam/gofakes3-- 将依赖从github.com/itsHenry35/gofakes3 v0.0.8切换到github.com/OpenListTeam/gofakes3 v0.8.0。旧版只有把所有分片缓存在内存的默认上传器;OpenListTeam fork 新增了可选的MultipartBackend接口,允许后端自行流式处理分片,这是实现分片上传的前提。同步更新server/s3下 8 个文件的 import 路径。feat(server/s3): support multipart upload-- 在s3Backend上实现gofakes3.MultipartBackend,把分片上传四个操作(Initiate / UploadPart / Complete / Abort)路由到 OpenList 自有后端:每个分片流式写入本地临时文件,完成时按分片号顺序拼合并写入底层存储。feat(server/s3): reap abandoned multipart uploads-- 回收被遗弃的分片上传,避免临时文件无限累积。为什么需要:此前分片上传回退到 gofakes3 默认的内存上传器,会把每个分片都缓存在内存中,大文件分片上传可能耗尽内存;且客户端不 complete/abort 的孤儿上传会一直占用磁盘。现在改为按分片落盘(内存占用恒定),并由后台 reaper 自动清理超时未活动的上传。
用户可感知的行为变化:S3 客户端(awscli / s3cmd / rclone 等)的分片上传现在可用,不再因内存限制而失败;被遗弃的分片上传会在 TTL 后被自动回收。
重要实现变化:
OpenListTeam/gofakes3 v0.8.0(baseBackend接口兼容,既有单分片PutObject等行为不变)server/s3/multipart.go,实现gofakes3.MultipartBackend(CreateMultipartUpload / UploadPart / CompleteMultipartUpload / AbortMultipartUpload)s3Backend新增uploads sync.Map跟踪进行中的上传;每个上传记录lastActivity,在创建与每次分片上传时更新PutObject主体抽取为可复用的putStream,与分片 Complete 路径共享(目录创建、元数据、忽略规则一致);单分片PutObject行为不变conf.Conf.TempDir下的s3-multipart-*目录;返回 S3 规范的分片 etag("<md5ofmd5s>-N")ErrIncompleteBody;abort 幂等;Complete 失败时保留上传以便客户端重试TTL/4(钳制[10s, 1h])间隔回收lastActivity超过 TTL 的上传及其临时目录;启动时额外清理上次进程崩溃残留的s3-multipart-*目录(仅清理超过 TTL 的,避免误伤并发启动的兄弟实例)s3.multipart_ttl(envS3_MULTIPART_TTL),用time.ParseDuration解析,空/非法/<=0 回退默认24h配置/存储/API/兼容性:新增一个可选配置项
s3.multipart_ttl(默认 24h,不配置即用默认值);不修改已有 API、存储格式或迁移行为。This PR has breaking changes.
/ 此 PR 包含破坏性变更。
This PR changes public API, config, storage format, or migration behavior.
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
This PR requires corresponding changes in related repositories.
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
Related Issues / 关联 Issue
Related to #460
Testing / 测试
go test ./...执行过的命令与结果(平台:macOS arm64,Go 1.26.5):
go build ./server/s3/、go build ./server/、go build ./internal/conf/-- 通过go vet ./server/...、go vet ./internal/conf/-- 干净gofmt -l server/s3/*.go internal/conf/config.go-- 干净go test ./server/s3/ -count=1-- 通过,包含新增测试:TestMultipartUploadEndToEnd:在真实 Local 驱动上 create->3 分片->complete,校验落盘文件内容为拼接结果、etag 为"<hex>-3";同号重传覆盖;短读->ErrIncompleteBody;未知 upload->ErrNoSuchUpload;越界分片号->ErrInvalidPart;乱序->ErrInvalidPartOrder;错 etag->ErrInvalidPart;缺分片->ErrInvalidPart;失败的 complete 保留上传可重试TestMultipartAbort:abort 删除临时目录并移除记录,且对未知 upload 幂等TestMultipartReapExpired:lastActivity超过 TTL 的上传被回收且临时目录删除,活跃上传保留TestMultipartCleanupStaleDirs:超过 TTL 的s3-multipart-*残留目录被清理,新目录保留go test ./...:server/s3通过。其余失败均与本次改动无关且为环境/既有问题--pkg/aria2/rpc:需连接localhost:6800的 aria2 守护进程(本机禁网)drivers/teldrive、drivers/webdav:需外部服务器/凭据internal/netTestNewOSSClientUsesEnvironmentHTTPSProxy:仓库既有失败(expected *http.Transport, got *net.safeTransport)drivers/*、internal/offline_download/*的 build failed:并行go test ./...时模块缓存写入竞争所致,单独go build正常server/s3(仅server/s3.go导入),不受本次改动影响未执行:对运行中的 OpenList 实例用 awscli / rclone 做手动联调(如维护者需要可补)。
Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。/ 我已在适用情况下请求相关维护者或代码所有者审查。
AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
Refactoring / 重构
Documentation / 文档
Tests / 测试
Translation / 翻译
Review assistance / 审查辅助
I have reviewed and validated all AI-assisted content included in this PR.
/ 我已审核并验证此 PR 中的所有 AI 辅助内容。
I have ensured that all AI-assisted commits include
Co-Authored-Byattribution./ 我已确保所有 AI 辅助提交都包含
Co-Authored-By归属信息。I can reproduce all AI-assisted content included in this PR without any AI tools.
/ 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。
Known Limitations / 已知限制
conf.Conf.TempDir,大文件上传会占用等量本地磁盘(相对原内存缓冲已是改进,为流式后端的固有取舍)。